// gelatin.js
//
// See README for a description of gelatin.
//
// It efficiently walks the file system, so, for example, it doesn't verify that
// the dependencies for a package are installed (specified via the
// 'dependencies' field in package.json).  That should be a separate tool.
//
// TODO
// - require('oil/log') and require('./log') could be the same module
//   - make sure the memoization accounts for this
//
// Nice to have:
//
// This code
//
//   var foo = require('foo');
//       bar = require('bar');
//
// contains a bug.  The first ; should be a comma.  The code works, but gelatin
// will fail silently.  Would be nice to catch this automatically.
//
// Also would be nice to scan for stray require() statements.

var args = require('args'),
    json = require('json'),
    os = require('os');

var jsontemplate = require('json-template'),
    oil = require('oil'),
    log = require('oil/log'),
    path = require('oil/path'),
    output = require('gelatin/output'),
    parsing = require('gelatin/fast-parsing'),
    packages = require('gelatin/packages'),
    gelatinRuntime = require('gelatin/runtime');


// Takes an array of deps, and a base module, and resolves all the modName
// entries to be absolute and not relative
var resolveRelativeRequires = exports.resolveRelativeRequires = 
function(deps, base) {
  for (var i=0; i<deps.length; i++) {
    var dep = deps[i];
    if (dep.modName.charAt(0) === '.') {
      dep.modName = path.normalize(base, '..', dep.modName);
    }
  }
};

// A dependency is a module referenced from another module.
//
// Args:
//   varName: The variable name, e.g. "jsontemplate"
//   modName: The module identifier, e.g. "json-template"
//   files: The files that this module lives in (modules can be split across
//     files) Array of {fullPath: handle:} objects
//   deps: Array of Dependency instances
var Dependency = exports.Dependency = function(varName, modName, filename) {
  this.varName = varName;
  this.modName = modName;

  // optional filename that the dependency appears in
  this.filename = filename || null;

  this.files = null;
  this.deps = [];
};

Dependency.prototype = {

  // Call this after resolving the files
  setFiles: function(files) {
    this.files = files;
  },

  // Opens all the files in this module, parses them, and fills in the deps
  // and files[*].offset fields.
  parse: function() {
    for (var i=0; i<this.files.length; i++) {
      var unit = this.files[i];

      log.info('Parsing %s', unit.fullPath);

      log.push();
      var contents = unit.handle.read();
      var extracted = parsing.parseDeps(contents);
      log.pop();

      resolveRelativeRequires(extracted.deps, this.modName);

      for (var j=0; j<extracted.deps.length; j++) {
        var d = extracted.deps[j],
            newDep = new Dependency(d.varName, d.modName, unit.fullPath);
        this.deps.push(newDep);
      }

      unit.offset = extracted.offset;
    }
  },

  toString: function() {
    return this.modName + ' ' + this.filesString();
  },

  // TODO: perhaps use jQuery's $.map().join()
  filesString: function() {
    var s = '';
    for (var i=0; i<this.files.length; i++) {
      var f = this.files[i];
      s += f.fullPath + ' (' + f.offset + '), ';
    }
    return s;
  },

  depsString: function() {
    var s = '';
    for (var i=0; i<this.deps.length; i++) {
      s += this.deps[i].modName + ', ';
    }
    return s;
  }
};


// Topological sort via DFS
//
// Args:
//   initial: importable module
//   packageSet: PackageSet instance
//   excludes: Array of module identifiers to exclude
//   engine: An engine name like 'rhino', or undefined
var crawlDeps = exports.crawlDeps = function(
    initial, packageSet, excludes, engine) {

  var visited = {},
      sorted = [];

  // Args:
  //  mod: module record with fullPath, modName, etc.
  function visit(dependency) {
    var modName = dependency.modName;
    log.info('Visit %s', modName);
    if (visited[modName]) {
      return;
    }
    visited[modName] = true;

    log.info('Parsing');

    // Sets the .files and .deps attributes
    dependency.parse();

    log.info('Done parsing');

    for (var i=0; i<dependency.deps.length; i++) {
      var d = dependency.deps[i];
    }

    log.info(
        '%s: Files %s  Dependencies: %s', 
        modName, dependency.filesString(), dependency.depsString());

    for (var i=0; i<dependency.deps.length; i++) {
      var d = dependency.deps[i];

      if (excludes[d.modName]) {
        log.info("Excluding module '%s'", modName);
        // Make sure this dependency is not an argument either.  I think it's
        // safe to delete while iterating this way in JS.
        delete dependency.deps[i];
        continue;
      }
      d.setFiles(packageSet.findModule(d.modName, engine, d.filename));
      visit(d);
    }
    sorted.push(dependency);
  }

  var dependency = new Dependency(initial, initial, '<command line>'),
      files = packageSet.findModule(initial, engine, dependency.filename);
  dependency.setFiles(files);
  visit(dependency);

  return sorted;
};

var parseArgs = function(argv) {

  var parser = new args.Parser();

  parser.usage('<importable module>');

  parser.help(
      'Starting from an importable module, crawls its dependencies and\n' + 
      'packages them into a single file\n'
      );

  parser.option('-p', '--packages', 'packagesList')
      .help('List of single packages to put on the search path')
      .set();

  parser.option('-d', '--packages-dir', 'packagesDir')
      .help('Package path, e.g. NARWHAL_PATH')
      .set();

  parser.option('-o', '--out', 'output')
      .help('File to write the archive to, - for stdout')
      .def('-')
      .set();

  parser.option('-i', '--include', 'include')
      .help('Comma-separated list of files to include BEFORE all modules.\n' + 
            'These can be use to modify primordials, etc.')
      .set();

  parser.option('-x', '--exclude', 'exclude')
      .help('Comma-separated list of module names to exclude.\n' + 
            'Dependencies are also excluded.')
      .set();

  parser.option('-r', '--runtime', 'runtime')
      .help('Comma-separated list of runtime module names.\n' + 
            "These are like excluded modules, except they're require()d at\n" +
            'the beginning of the archive')
      .set();

  parser.option('-e', '--engine', 'engine')
      .help('Also include code from PACKAGE/engines/{engine} in front of\n' + 
            'the engine-independent portion.')
      .set();

  parser.option('-n', '--init', 'init')
      .help('Initialitions to that go after module definition but before\n' + 
            'module initialization.')
      .set();

  parser.option('-m', '--main', 'main')
      .help('File containing a template for the main program, to be\n' +
            'included AFTER all modules.  Generally you will call\n' +
            '{initial}.main()')
      .set();

  parser.option('-s', '--shebang', 'shebang')
      .help('Shebang line to define the interpreter (omit #!)')
      .set();

  parser.option('-h', '--help')
      .action(parser.printHelp);

  return parser.parse(argv);
};
  
var makePackagesList = function(fs, packagesList, packagesDir) {
  packagesList = packagesList.split(path.pathSep);

  if (packagesDir) {
    try {
      var entries = fs.list(packagesDir);
    } catch(e) {
      log.error("Couldn't list dir %s: %s", packagesDir, e);
      return packagesList;
    }
    for (var j=0; j<entries.length; j++) {
      packagesList.push(path.join(packagesDir, entries[j]));
    }
  }
  return packagesList;
}

exports.main = function(argv) {
  flags = parseArgs(argv);

  var initial = argv[0];
  if (!initial) {
    log.error('Need a module as the first argument.');
    os.exit(2);
  }

  var rootfs = new oil.FileSystem('');

  var packagesList = makePackagesList(
      rootfs, flags.packagesList, flags.packagesDir);

  log.info('packages ' + packagesList);

  if (packagesList.length === 0) {
    log.error('Specify paths using -d and/or -p');
    os.exit(2);
  }

  var excludes = {},  // for lookup
      runtimeList = [];
  if (flags.exclude) {
    var e = flags.exclude.split(',');
    for (var i=0; i<e.length; i++) {
      excludes[e[i]] = true;
    }
  }

  if (flags.runtime) {
    var r = flags.runtime.split(',');
    for (var i=0; i<r.length; i++) {
      excludes[r[i]] = true;
      runtimeList.push(r[i]);
    }
  }

  log.info('Searching for %s', initial);

  dataTree = gelatinRuntime.dataTree(flags.command);

  try {
    log.info('Making packages');
    var packageSet = packages.makePackages(packagesList);

    log.info('Crawling');
    var sorted = crawlDeps(initial, packageSet, excludes, flags.engine);

    log.info('Done Crawling');

    var templateData = {
        modules: sorted,
        userInit: flags.init,
        shebang: flags.shebang,
        runtime: runtimeList
        };

    // BEFORE modules
    if (flags.include) {
      var filenames = flags.include.split(','),
          includes = [];
      
      for (var i=0; i<filenames.length; i++) {
        var filename = filenames[i],
            contents = rootfs.contentsOf(filename);
        includes.push({contents: contents, filename: filename});
      }
      templateData.includes = includes;
    }

    // AFTER modules
    if (flags.main) { 
      var mainTemplate = rootfs.contentsOf(flags.main),
          mainCode = output.expandMain(mainTemplate, {initial: initial});
      templateData.main = mainCode;
    }

    log.info('');
    log.info('Sorted:');
    log.push();
    for (var i=0; i<sorted.length; i++) {
      log.info('%s', sorted[i]);
    }
    log.pop();
    log.info('---');

    var outfile;
    if (flags.output === '-') {
      outfile = system.stdout;
    } else {
      var fs = new oil.FileSystem();
      outfile = fs.open(flags.output, 'w');
      log.info('Writing to %s', flags.output);
    }

    output.write(outfile, templateData, dataTree);
    outfile.close();
    log.info('Done');

  } catch (e) {
    print('Uncaught exception:');

    if (e.stack) {
      print(e.stack);
    }
    if (e.name) {
      print(e.name);
      print(e.message);
    }
    print(e);
  }
};
